From aafc3fbe31b5e687f94fd2b76a3d7713e828a017 Mon Sep 17 00:00:00 2001 From: Godnight1006 Date: Fri, 14 Nov 2025 11:38:56 +0800 Subject: [PATCH] Fix #275: Make openai dataflow compatible with Gemini and OpenRouter - Replace OpenAI-specific responses.create() API with standard chat.completions.create() - Update get_stock_news_openai, get_global_news_openai, and get_fundamentals_openai - Add comprehensive tests for OpenAI, Gemini, and OpenRouter compatibility - All functions now use standard OpenAI-compatible chat completion API - Fixes RuntimeError: All vendor implementations failed for method 'get_global_news' - Fixes RuntimeError: All vendor implementations failed for method 'get_indicators' The issue was that the openai vendor functions used OpenAI-specific API features (responses.create with web_search_preview tools) that are not supported by Gemini or OpenRouter. By switching to the standard chat completions API, these functions now work with any OpenAI-compatible provider. --- tests/__init__.py | 2 + tests/test_openai_dataflow.py | 135 ++++++++++++++++++++++++++++++ tradingagents/dataflows/openai.py | 119 +++++++++++++------------- 3 files changed, 199 insertions(+), 57 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_openai_dataflow.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..ce279046 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Tests for TradingAgents + diff --git a/tests/test_openai_dataflow.py b/tests/test_openai_dataflow.py new file mode 100644 index 00000000..c028ac5d --- /dev/null +++ b/tests/test_openai_dataflow.py @@ -0,0 +1,135 @@ +""" +Tests for openai dataflow module to ensure compatibility with different LLM providers. +This test reproduces issue #275 where Gemini and OpenRouter fail with openai vendor. +""" +import pytest +from unittest.mock import Mock, patch, MagicMock +from tradingagents.dataflows.openai import ( + get_stock_news_openai, + get_global_news_openai, + get_fundamentals_openai +) + + +class TestOpenAIDataflowCompatibility: + """Test that openai dataflow functions work with different LLM providers.""" + + @pytest.fixture + def mock_config_openai(self): + """Mock config for OpenAI provider.""" + return { + "backend_url": "https://api.openai.com/v1", + "quick_think_llm": "gpt-4o-mini", + "llm_provider": "openai" + } + + @pytest.fixture + def mock_config_gemini(self): + """Mock config for Google Gemini provider.""" + return { + "backend_url": "https://generativelanguage.googleapis.com/v1", + "quick_think_llm": "gemini-2.0-flash", + "llm_provider": "google" + } + + @pytest.fixture + def mock_config_openrouter(self): + """Mock config for OpenRouter provider.""" + return { + "backend_url": "https://openrouter.ai/api/v1", + "quick_think_llm": "deepseek/deepseek-chat-v3-0324:free", + "llm_provider": "openrouter" + } + + @patch('tradingagents.dataflows.openai.get_config') + @patch('tradingagents.dataflows.openai.OpenAI') + def test_get_global_news_with_openai(self, mock_openai_class, mock_get_config, mock_config_openai): + """Test get_global_news_openai works with OpenAI provider.""" + mock_get_config.return_value = mock_config_openai + + # Mock the OpenAI client and response + mock_client = Mock() + mock_openai_class.return_value = mock_client + + # Mock chat completion response (standard API) + mock_response = Mock() + mock_response.choices = [Mock()] + mock_response.choices[0].message.content = "Test news content" + mock_client.chat.completions.create.return_value = mock_response + + # Call the function + result = get_global_news_openai("2024-11-09", 7, 5) + + # Verify it was called + assert mock_client.chat.completions.create.called + assert result == "Test news content" + + @patch('tradingagents.dataflows.openai.get_config') + @patch('tradingagents.dataflows.openai.OpenAI') + def test_get_global_news_with_gemini(self, mock_openai_class, mock_get_config, mock_config_gemini): + """Test get_global_news_openai works with Gemini provider (via OpenAI-compatible API).""" + mock_get_config.return_value = mock_config_gemini + + # Mock the OpenAI client and response + mock_client = Mock() + mock_openai_class.return_value = mock_client + + # Mock chat completion response + mock_response = Mock() + mock_response.choices = [Mock()] + mock_response.choices[0].message.content = "Test Gemini news content" + mock_client.chat.completions.create.return_value = mock_response + + # Call the function - should not raise an error + result = get_global_news_openai("2024-11-09", 7, 5) + + # Verify it was called with standard chat completion API + assert mock_client.chat.completions.create.called + assert result == "Test Gemini news content" + + @patch('tradingagents.dataflows.openai.get_config') + @patch('tradingagents.dataflows.openai.OpenAI') + def test_get_global_news_with_openrouter(self, mock_openai_class, mock_get_config, mock_config_openrouter): + """Test get_global_news_openai works with OpenRouter provider.""" + mock_get_config.return_value = mock_config_openrouter + + # Mock the OpenAI client and response + mock_client = Mock() + mock_openai_class.return_value = mock_client + + # Mock chat completion response + mock_response = Mock() + mock_response.choices = [Mock()] + mock_response.choices[0].message.content = "Test OpenRouter news content" + mock_client.chat.completions.create.return_value = mock_response + + # Call the function - should not raise an error + result = get_global_news_openai("2024-11-09", 7, 5) + + # Verify it was called with standard chat completion API + assert mock_client.chat.completions.create.called + assert result == "Test OpenRouter news content" + + @patch('tradingagents.dataflows.openai.get_config') + @patch('tradingagents.dataflows.openai.OpenAI') + def test_get_fundamentals_with_different_providers(self, mock_openai_class, mock_get_config, mock_config_gemini): + """Test get_fundamentals_openai works with different providers.""" + mock_get_config.return_value = mock_config_gemini + + # Mock the OpenAI client and response + mock_client = Mock() + mock_openai_class.return_value = mock_client + + # Mock chat completion response + mock_response = Mock() + mock_response.choices = [Mock()] + mock_response.choices[0].message.content = "Test fundamentals data" + mock_client.chat.completions.create.return_value = mock_response + + # Call the function + result = get_fundamentals_openai("AAPL", "2024-11-09") + + # Verify it was called + assert mock_client.chat.completions.create.called + assert result == "Test fundamentals data" + diff --git a/tradingagents/dataflows/openai.py b/tradingagents/dataflows/openai.py index 91a2258b..15ed0e58 100644 --- a/tradingagents/dataflows/openai.py +++ b/tradingagents/dataflows/openai.py @@ -3,105 +3,110 @@ from .config import get_config def get_stock_news_openai(query, start_date, end_date): + """ + Retrieve stock news using LLM provider configured in backend_url. + Compatible with OpenAI, Gemini (via OpenAI-compatible API), and OpenRouter. + + Args: + query: Stock ticker or search query + start_date: Start date for news search + end_date: End date for news search + + Returns: + str: News content as text + """ config = get_config() client = OpenAI(base_url=config["backend_url"]) - response = client.responses.create( + # Use standard chat completions API for compatibility with all providers + response = client.chat.completions.create( model=config["quick_think_llm"], - input=[ + messages=[ { "role": "system", - "content": [ - { - "type": "input_text", - "text": f"Can you search Social Media for {query} from {start_date} to {end_date}? Make sure you only get the data posted during that period.", - } - ], - } - ], - text={"format": {"type": "text"}}, - reasoning={}, - tools=[ + "content": "You are a financial news analyst. Search and summarize relevant news from social media and news sources." + }, { - "type": "web_search_preview", - "user_location": {"type": "approximate"}, - "search_context_size": "low", + "role": "user", + "content": f"Can you search Social Media for {query} from {start_date} to {end_date}? Make sure you only get the data posted during that period." } ], temperature=1, - max_output_tokens=4096, + max_tokens=4096, top_p=1, - store=True, ) - return response.output[1].content[0].text + return response.choices[0].message.content def get_global_news_openai(curr_date, look_back_days=7, limit=5): + """ + Retrieve global news using LLM provider configured in backend_url. + Compatible with OpenAI, Gemini (via OpenAI-compatible API), and OpenRouter. + + Args: + curr_date: Current date in yyyy-mm-dd format + look_back_days: Number of days to look back (default 7) + limit: Maximum number of articles to return (default 5) + + Returns: + str: Global news content as text + """ config = get_config() client = OpenAI(base_url=config["backend_url"]) - response = client.responses.create( + # Use standard chat completions API for compatibility with all providers + response = client.chat.completions.create( model=config["quick_think_llm"], - input=[ + messages=[ { "role": "system", - "content": [ - { - "type": "input_text", - "text": f"Can you search global or macroeconomics news from {look_back_days} days before {curr_date} to {curr_date} that would be informative for trading purposes? Make sure you only get the data posted during that period. Limit the results to {limit} articles.", - } - ], - } - ], - text={"format": {"type": "text"}}, - reasoning={}, - tools=[ + "content": "You are a financial news analyst. Search and summarize relevant global and macroeconomic news for trading purposes." + }, { - "type": "web_search_preview", - "user_location": {"type": "approximate"}, - "search_context_size": "low", + "role": "user", + "content": f"Can you search global or macroeconomics news from {look_back_days} days before {curr_date} to {curr_date} that would be informative for trading purposes? Make sure you only get the data posted during that period. Limit the results to {limit} articles." } ], temperature=1, - max_output_tokens=4096, + max_tokens=4096, top_p=1, - store=True, ) - return response.output[1].content[0].text + return response.choices[0].message.content def get_fundamentals_openai(ticker, curr_date): + """ + Retrieve fundamental data using LLM provider configured in backend_url. + Compatible with OpenAI, Gemini (via OpenAI-compatible API), and OpenRouter. + + Args: + ticker: Stock ticker symbol + curr_date: Current date in yyyy-mm-dd format + + Returns: + str: Fundamental data as text (table format with PE/PS/Cash flow etc) + """ config = get_config() client = OpenAI(base_url=config["backend_url"]) - response = client.responses.create( + # Use standard chat completions API for compatibility with all providers + response = client.chat.completions.create( model=config["quick_think_llm"], - input=[ + messages=[ { "role": "system", - "content": [ - { - "type": "input_text", - "text": f"Can you search Fundamental for discussions on {ticker} during of the month before {curr_date} to the month of {curr_date}. Make sure you only get the data posted during that period. List as a table, with PE/PS/Cash flow/ etc", - } - ], - } - ], - text={"format": {"type": "text"}}, - reasoning={}, - tools=[ + "content": "You are a financial analyst. Search and provide fundamental data for stocks in a structured table format." + }, { - "type": "web_search_preview", - "user_location": {"type": "approximate"}, - "search_context_size": "low", + "role": "user", + "content": f"Can you search Fundamental for discussions on {ticker} during of the month before {curr_date} to the month of {curr_date}. Make sure you only get the data posted during that period. List as a table, with PE/PS/Cash flow/ etc" } ], temperature=1, - max_output_tokens=4096, + max_tokens=4096, top_p=1, - store=True, ) - return response.output[1].content[0].text \ No newline at end of file + return response.choices[0].message.content \ No newline at end of file