From 01ef5bf64fe399f5898ca06e5f048bc18adaaf62 Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Thu, 23 Jul 2026 00:28:28 +0530 Subject: [PATCH 1/2] Python: Support OpenAI instructions in Responses API --- .../agent_framework_openai/_chat_client.py | 18 ++--- .../tests/openai/test_openai_chat_client.py | 78 +++++-------------- 2 files changed, 30 insertions(+), 66 deletions(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index de703509da..2355202e4d 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -56,7 +56,6 @@ TextSpanRegion, UsageDetails, detect_media_type_from_base64, - prepend_instructions_to_messages, validate_tool_mode, ) from agent_framework.exceptions import ( @@ -205,6 +204,10 @@ class OpenAIChatOptions(ChatOptions[ResponseFormatT], Generic[ResponseFormatT], See: https://platform.openai.com/docs/api-reference/responses/create """ + instructions: str + """Ephemeral per-request instructions that apply only to the current response. + This does not persist in the conversation state across turns.""" + # Responses API-specific parameters include: list[str] @@ -1372,7 +1375,7 @@ async def _prepare_options( "logit_bias", # not supported "seed", # not supported "stop", # not supported - "instructions", # already added as system message + # "instructions" removed: now passed natively to Responses API for ephemeral per-request steering "response_format", # handled separately "conversation_id", # handled separately "tool_choice", # handled separately @@ -1381,13 +1384,10 @@ async def _prepare_options( run_options: dict[str, Any] = {k: v for k, v in options.items() if k not in exclude_keys and v is not None} # messages - # Handle instructions by prepending to messages as system message - # Only prepend instructions for the first turn (when no conversation/response ID exists) - conversation_id = options.get("conversation_id") - if (instructions := options.get("instructions")) and not conversation_id: - # First turn: prepend instructions as system message - messages = prepend_instructions_to_messages(list(messages), instructions, role="system") - # Continuation turn: instructions already exist in conversation context, skip prepending + # Per-request "instructions" are no longer prepended as system messages. + # They are now passed natively to the Responses API via run_options (see exclude_keys above), + # enabling ephemeral per-request steering that does NOT persist across turns + request_uses_service_side_storage = False for key in ("conversation_id", "previous_response_id", "conversation"): value = options.get(key) diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 0497d3ddf0..53be3ee24f 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -410,10 +410,14 @@ async def test_get_response_with_all_parameters() -> None: assert len(run_options["tools"]) == 1 assert run_options["tools"][0]["type"] == "function" assert run_options["tools"][0]["name"] == "get_weather" - assert run_options["input"][0]["role"] == "system" - assert run_options["input"][0]["content"][0]["text"] == "You are a helpful assistant" - assert run_options["input"][1]["role"] == "user" - assert run_options["input"][1]["content"][0]["text"] == "Test message" + + # Verify instructions are passed natively, not as a system message + assert run_options["instructions"] == "You are a helpful assistant" + + # Verify the input only contains the user message + assert len(run_options["input"]) == 1 + assert run_options["input"][0]["role"] == "user" + assert run_options["input"][0]["content"][0]["text"] == "Test message" @pytest.mark.asyncio @@ -5417,70 +5421,30 @@ def _create_mock_responses_text_response(*, response_id: str) -> MagicMock: return mock_response -async def test_instructions_sent_first_turn_then_skipped_for_continuation() -> None: - client = OpenAIChatClient(model="test-model", api_key="test-key") - mock_response = _create_mock_responses_text_response(response_id="resp_123") - - with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: - await client.get_response( - messages=[Message(role="user", contents=["Hello"])], - options={"instructions": "Reply in uppercase."}, - ) - - first_input_messages = mock_create.call_args.kwargs["input"] - assert len(first_input_messages) == 2 - assert first_input_messages[0]["role"] == "system" - assert any("Reply in uppercase" in str(c) for c in first_input_messages[0]["content"]) - assert first_input_messages[1]["role"] == "user" - - await client.get_response( - messages=[Message(role="user", contents=["Tell me a joke"])], - options={ - "instructions": "Reply in uppercase.", - "conversation_id": "resp_123", - }, - ) - - second_input_messages = mock_create.call_args.kwargs["input"] - assert len(second_input_messages) == 1 - assert second_input_messages[0]["role"] == "user" - assert not any(message["role"] == "system" for message in second_input_messages) - - -@pytest.mark.parametrize("conversation_id", ["resp_456", "conv_abc123"]) -async def test_instructions_not_repeated_for_continuation_ids( - conversation_id: str, +@pytest.mark.parametrize("conversation_id", [None, "resp_456", "conv_abc123"]) +async def test_instructions_passed_natively_not_as_system_message( + conversation_id: str | None, ) -> None: + """Test that instructions are passed to the Responses API natively and not prepended to messages.""" client = OpenAIChatClient(model="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_456") with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: - await client.get_response( - messages=[Message(role="user", contents=["Continue conversation"])], - options={"instructions": "Be helpful.", "conversation_id": conversation_id}, - ) - - input_messages = mock_create.call_args.kwargs["input"] - assert len(input_messages) == 1 - assert input_messages[0]["role"] == "user" - assert not any(message["role"] == "system" for message in input_messages) + options = {"instructions": "Reply in uppercase."} + if conversation_id: + options["conversation_id"] = conversation_id - -async def test_instructions_included_without_conversation_id() -> None: - client = OpenAIChatClient(model="test-model", api_key="test-key") - mock_response = _create_mock_responses_text_response(response_id="resp_new") - - with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: await client.get_response( messages=[Message(role="user", contents=["Hello"])], - options={"instructions": "You are a helpful assistant."}, + options=options, ) + assert mock_create.call_args.kwargs.get("instructions") == "Reply in uppercase." + input_messages = mock_create.call_args.kwargs["input"] - assert len(input_messages) == 2 - assert input_messages[0]["role"] == "system" - assert any("helpful assistant" in str(c) for c in input_messages[0]["content"]) - assert input_messages[1]["role"] == "user" + assert len(input_messages) == 1 + assert input_messages[0]["role"] == "user" + assert not any(message.get("role") == "system" for message in input_messages) def test_with_callable_api_key() -> None: From 7ab3ffea2fd7b4d30ab759e0878cdc0eeca9ffee Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Sat, 25 Jul 2026 00:01:26 +0530 Subject: [PATCH 2/2] fix: address PR comments and fix typing for OpenAIChatOptions --- .../openai/agent_framework_openai/_chat_client.py | 9 --------- .../openai/tests/openai/test_openai_chat_client.py | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 2355202e4d..53ddfd6316 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -204,10 +204,6 @@ class OpenAIChatOptions(ChatOptions[ResponseFormatT], Generic[ResponseFormatT], See: https://platform.openai.com/docs/api-reference/responses/create """ - instructions: str - """Ephemeral per-request instructions that apply only to the current response. - This does not persist in the conversation state across turns.""" - # Responses API-specific parameters include: list[str] @@ -1375,7 +1371,6 @@ async def _prepare_options( "logit_bias", # not supported "seed", # not supported "stop", # not supported - # "instructions" removed: now passed natively to Responses API for ephemeral per-request steering "response_format", # handled separately "conversation_id", # handled separately "tool_choice", # handled separately @@ -1383,10 +1378,6 @@ async def _prepare_options( } run_options: dict[str, Any] = {k: v for k, v in options.items() if k not in exclude_keys and v is not None} - # messages - # Per-request "instructions" are no longer prepended as system messages. - # They are now passed natively to the Responses API via run_options (see exclude_keys above), - # enabling ephemeral per-request steering that does NOT persist across turns request_uses_service_side_storage = False for key in ("conversation_id", "previous_response_id", "conversation"): diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 53be3ee24f..011edb6eb7 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -57,7 +57,7 @@ from pydantic import BaseModel from pytest import param -from agent_framework_openai import OpenAIChatClient +from agent_framework_openai import OpenAIChatClient, OpenAIChatOptions from agent_framework_openai._chat_client import OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY, RawOpenAIChatClient from agent_framework_openai._exceptions import OpenAIContentFilterException @@ -5430,7 +5430,7 @@ async def test_instructions_passed_natively_not_as_system_message( mock_response = _create_mock_responses_text_response(response_id="resp_456") with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: - options = {"instructions": "Reply in uppercase."} + options: OpenAIChatOptions = {"instructions": "Reply in uppercase."} if conversation_id: options["conversation_id"] = conversation_id