From 3ff28f3559c3598cd5efb6f3469735e14ce7f3e0 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 20:34:03 +0000 Subject: [PATCH] fix: use OpenAI Responses API for native models Enable use_responses_api for native OpenAI provider, which supports reasoning_effort with function tools across all model families. Removes the UnifiedChatOpenAI subclass workaround. Closes #403 --- tradingagents/llm_clients/openai_client.py | 69 +++++++++++----------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 4605c1f9..c314d077 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -6,28 +6,28 @@ from langchain_openai import ChatOpenAI from .base_client import BaseLLMClient from .validators import validate_model +# Kwargs forwarded from user config to ChatOpenAI +_PASSTHROUGH_KWARGS = ( + "timeout", "max_retries", "reasoning_effort", + "api_key", "callbacks", "http_client", "http_async_client", +) -class UnifiedChatOpenAI(ChatOpenAI): - """ChatOpenAI subclass that strips temperature/top_p for GPT-5 family models. - - GPT-5 family models use reasoning natively. temperature/top_p are only - accepted when reasoning.effort is 'none'; with any other effort level - (or for older GPT-5/GPT-5-mini/GPT-5-nano which always reason) the API - rejects these params. Langchain defaults temperature=0.7, so we must - strip it to avoid errors. - - Non-GPT-5 models (GPT-4.1, xAI, Ollama, etc.) are unaffected. - """ - - def __init__(self, **kwargs): - if "gpt-5" in kwargs.get("model", "").lower(): - kwargs.pop("temperature", None) - kwargs.pop("top_p", None) - super().__init__(**kwargs) +# Provider base URLs and API key env vars +_PROVIDER_CONFIG = { + "xai": ("https://api.x.ai/v1", "XAI_API_KEY"), + "openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"), + "ollama": ("http://localhost:11434/v1", None), +} class OpenAIClient(BaseLLMClient): - """Client for OpenAI, Ollama, OpenRouter, and xAI providers.""" + """Client for OpenAI, Ollama, OpenRouter, and xAI providers. + + For native OpenAI models, uses the Responses API (/v1/responses) which + supports reasoning_effort with function tools across all model families + (GPT-4.1, GPT-5). Third-party compatible providers (xAI, OpenRouter, + Ollama) use standard Chat Completions. + """ def __init__( self, @@ -43,27 +43,30 @@ class OpenAIClient(BaseLLMClient): """Return configured ChatOpenAI instance.""" llm_kwargs = {"model": self.model} - if self.provider == "xai": - llm_kwargs["base_url"] = "https://api.x.ai/v1" - api_key = os.environ.get("XAI_API_KEY") - if api_key: - llm_kwargs["api_key"] = api_key - elif self.provider == "openrouter": - llm_kwargs["base_url"] = "https://openrouter.ai/api/v1" - api_key = os.environ.get("OPENROUTER_API_KEY") - if api_key: - llm_kwargs["api_key"] = api_key - elif self.provider == "ollama": - llm_kwargs["base_url"] = "http://localhost:11434/v1" - llm_kwargs["api_key"] = "ollama" # Ollama doesn't require auth + # Provider-specific base URL and auth + if self.provider in _PROVIDER_CONFIG: + base_url, api_key_env = _PROVIDER_CONFIG[self.provider] + llm_kwargs["base_url"] = base_url + if api_key_env: + api_key = os.environ.get(api_key_env) + if api_key: + llm_kwargs["api_key"] = api_key + else: + llm_kwargs["api_key"] = "ollama" elif self.base_url: llm_kwargs["base_url"] = self.base_url - for key in ("timeout", "max_retries", "reasoning_effort", "api_key", "callbacks", "http_client", "http_async_client"): + # Forward user-provided kwargs + for key in _PASSTHROUGH_KWARGS: if key in self.kwargs: llm_kwargs[key] = self.kwargs[key] - return UnifiedChatOpenAI(**llm_kwargs) + # Native OpenAI: use Responses API for consistent behavior across + # all model families. Third-party providers use Chat Completions. + if self.provider == "openai": + llm_kwargs["use_responses_api"] = True + + return ChatOpenAI(**llm_kwargs) def validate_model(self) -> bool: """Validate model for the provider."""