diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 8257678584..58ae21b88f 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -844,8 +844,16 @@ def _convert_response_format(self, response_format: Mapping[str, Any]) -> dict[s def _get_conversation_id( self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None ) -> str | None: - """Get the conversation ID from the response if store is True.""" - if store is False: + """Get the server-managed conversation identifier when ``store=True`` is set. + + Only returns a non-None value when ``store=True`` is explicitly set to opt into + server-managed conversation mode (OpenAI native). In that mode, this returns + ``response.conversation.id`` when present; otherwise it falls back to + ``response.id``. When ``store`` is ``None`` (the default) or ``False``, + returns ``None`` so the executor uses full-history mode, which is compatible + with providers that do not support ``previous_response_id`` (e.g. OpenRouter). + """ + if store is not True: return None # If conversation ID exists, it means that we operate with conversation # so we use conversation ID as input and output. 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 31c3c26fe0..a4f97f454e 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -2780,6 +2780,24 @@ def test_parse_response_with_store_false() -> None: assert conversation_id is None +def test_parse_response_with_store_none_returns_none() -> None: + """Test _get_conversation_id returns None when store=None (default). + + Regression for https://github.com/microsoft/agent-framework/issues/5105 — + ensures full-history mode is used by default, which works with providers + that don't support previous_response_id (e.g. OpenRouter). + """ + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.id = "resp_123" + mock_response.conversation = MagicMock() + mock_response.conversation.id = "conv_456" + + assert client._get_conversation_id(mock_response, store=None) is None + assert client._get_conversation_id(mock_response, store=False) is None + + def test_parse_response_uses_response_id_when_no_conversation() -> None: """Test _get_conversation_id returns response ID when no conversation exists.""" client = OpenAIChatClient(model="test-model", api_key="test-key") @@ -3259,9 +3277,8 @@ def test_streaming_response_basic_structure() -> None: def test_streaming_response_created_type() -> None: - """Test streaming response with created type""" + """Test streaming response with created type sets conversation_id only when store=True.""" client = OpenAIChatClient(model="test-model", api_key="test-key") - chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} mock_event = MagicMock() @@ -3271,16 +3288,20 @@ def test_streaming_response_created_type() -> None: mock_event.response.conversation = MagicMock() mock_event.response.conversation.id = "conv_5678" - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) - + # store=True: server-managed mode — conversation_id is returned + response = client._parse_chunk_from_openai(mock_event, ChatOptions(store=True), function_call_ids) assert response.response_id == "resp_1234" assert response.conversation_id == "conv_5678" + # store=None (default): full-history mode — conversation_id is NOT returned + response = client._parse_chunk_from_openai(mock_event, ChatOptions(), function_call_ids) + assert response.response_id == "resp_1234" + assert response.conversation_id is None + def test_streaming_response_in_progress_type() -> None: - """Test streaming response with in_progress type""" + """Test streaming response with in_progress type sets conversation_id only when store=True.""" client = OpenAIChatClient(model="test-model", api_key="test-key") - chat_options = ChatOptions() function_call_ids: dict[int, tuple[str, str]] = {} mock_event = MagicMock() @@ -3290,11 +3311,16 @@ def test_streaming_response_in_progress_type() -> None: mock_event.response.conversation = MagicMock() mock_event.response.conversation.id = "conv_5678" - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) - + # store=True: server-managed mode — conversation_id is returned + response = client._parse_chunk_from_openai(mock_event, ChatOptions(store=True), function_call_ids) assert response.response_id == "resp_1234" assert response.conversation_id == "conv_5678" + # store=None (default): full-history mode — conversation_id is NOT returned + response = client._parse_chunk_from_openai(mock_event, ChatOptions(), function_call_ids) + assert response.response_id == "resp_1234" + assert response.conversation_id is None + def test_streaming_annotation_added_with_file_path() -> None: """Streaming `file_path` should attach as a text annotation, matching non-streaming."""