From 9b4c741d33c7c0ad8c2a79da52796ffd529df61a Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Mon, 14 Sep 2026 18:09:33 +0000 Subject: [PATCH] fix(llm): forward reasoning effort to GPT-6 and gate minimal thinking - reasoning_effort was forwarded only to IDs matching gpt-5 or the o-series, so GPT-6 models silently dropped the configured effort; match GPT-5 and later, with a version boundary so unrelated IDs do not match - Gemini Pro, 3.8+ and the -latest aliases reject thinking_level "minimal" with a 400; send it only to numbered Flash models before 3.8 and map it to "low" elsewhere, since aliases move between generations --- tests/test_google_thinking_level.py | 20 ++++++++++++++++++-- tests/test_openai_reasoning_effort.py | 5 +++-- tradingagents/llm_clients/google_client.py | 22 ++++++++++++++++++---- tradingagents/llm_clients/openai_client.py | 6 +++--- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/tests/test_google_thinking_level.py b/tests/test_google_thinking_level.py index f24ceff2f..e711c2aab 100644 --- a/tests/test_google_thinking_level.py +++ b/tests/test_google_thinking_level.py @@ -1,8 +1,8 @@ """Gemini thinking_level forwarding (Gemini 3.x). The catalog is Gemini 3.x only, which takes the string ``thinking_level`` -directly. Pro accepts low/high; Flash also accepts minimal/medium — an -unsupported "minimal" on Pro is mapped to "low". +directly. Pro, Gemini 3.8+ and the -latest aliases reject "minimal" with a 400, +so it is mapped to "low" there; numbered Flash models before 3.8 accept it. """ from unittest import mock @@ -35,6 +35,22 @@ def test_pro_remaps_minimal_to_low(): assert kw["thinking_level"] == "low" # Pro doesn't accept "minimal" +def test_flash_38_remaps_minimal_to_low(): + kw = _captured_kwargs("gemini-3.8-flash", thinking_level="minimal") + assert kw["thinking_level"] == "low" # 3.8 Flash 400s on "minimal" + + +def test_flash_38_keeps_supported_levels(): + kw = _captured_kwargs("gemini-3.8-flash", thinking_level="high") + assert kw["thinking_level"] == "high" + + +@pytest.mark.parametrize("alias", ["gemini-flash-latest", "gemini-pro-latest"]) +def test_latest_alias_remaps_minimal_to_low(alias): + # Aliases move between generations; gemini-flash-latest 400s on "minimal". + assert _captured_kwargs(alias, thinking_level="minimal")["thinking_level"] == "low" + + def test_pro_keeps_high(): kw = _captured_kwargs("gemini-3.1-pro-preview", thinking_level="high") assert kw["thinking_level"] == "high" diff --git a/tests/test_openai_reasoning_effort.py b/tests/test_openai_reasoning_effort.py index f58a4e20c..1d452ba27 100644 --- a/tests/test_openai_reasoning_effort.py +++ b/tests/test_openai_reasoning_effort.py @@ -17,9 +17,10 @@ from tradingagents.llm_clients.openai_client import ( "model,expected", [ ("gpt-5.5", True), ("gpt-5.4", True), ("gpt-5.4-mini", True), - ("gpt-5.5-pro", True), ("o1", True), ("o3-mini", True), + ("gpt-5.5-pro", True), ("gpt-6-astra", True), ("o1", True), ("o3-mini", True), ("gpt-4.1", False), ("gpt-4o", False), ("gpt-4o-mini", False), - ("gpt-3.5-turbo", False), + ("gpt-3.5-turbo", False), ("gpt-10", True), + ("gpt-5foo", False), ("gpt-60x", False), ("o3rd-party", False), ], ) def test_supports_reasoning_effort(model, expected): diff --git a/tradingagents/llm_clients/google_client.py b/tradingagents/llm_clients/google_client.py index 19ae8a213..134baf556 100644 --- a/tradingagents/llm_clients/google_client.py +++ b/tradingagents/llm_clients/google_client.py @@ -1,3 +1,4 @@ +import re from typing import Any from langchain_google_genai import ChatGoogleGenerativeAI @@ -5,6 +6,19 @@ from langchain_google_genai import ChatGoogleGenerativeAI from .base_client import BaseLLMClient, normalize_content from .validators import validate_model +_GEMINI_VERSION = re.compile(r"^gemini-(\d+)\.(\d+)") + + +def _accepts_minimal_thinking(model: str) -> bool: + """Whether ``thinking_level="minimal"`` is accepted: numbered Flash models + before 3.8. Pro, 3.8+ and version-less aliases (which move between + generations) are treated as rejecting it.""" + model_lc = model.lower() + match = _GEMINI_VERSION.match(model_lc) + return bool(match) and "pro" not in model_lc and ( + (int(match.group(1)), int(match.group(2))) < (3, 8) + ) + class NormalizedChatGoogleGenerativeAI(ChatGoogleGenerativeAI): """ChatGoogleGenerativeAI with normalized content output. @@ -42,12 +56,12 @@ class GoogleClient(BaseLLMClient): llm_kwargs["google_api_key"] = google_api_key # Gemini 3.x takes the string ``thinking_level`` (the integer - # ``thinking_budget`` was for the now-retired 2.5 line). Pro accepts - # low/high; Flash also accepts minimal/medium — so map an unsupported - # "minimal" on Pro to the nearest level it does accept. + # ``thinking_budget`` was for the now-retired 2.5 line). Pro, Gemini + # 3.8+ and the -latest aliases reject "minimal" with a 400; "low" is + # accepted everywhere, so it is the fallback. thinking_level = self.kwargs.get("thinking_level") if thinking_level: - if "pro" in self.model.lower() and thinking_level == "minimal": + if thinking_level == "minimal" and not _accepts_minimal_thinking(self.model): thinking_level = "low" llm_kwargs["thinking_level"] = thinking_level diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 5f78eb6e6..2caa728c1 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -168,11 +168,11 @@ _PASSTHROUGH_KWARGS = ( "api_key", "callbacks", "http_client", "http_async_client", ) -# OpenAI's ``reasoning_effort`` is only accepted by reasoning models — the GPT-5 -# family and the o-series. Non-reasoning models (gpt-4.1, gpt-4o, ...) 400 with +# OpenAI's ``reasoning_effort`` is only accepted by reasoning models — GPT-5 and +# later, and the o-series. Non-reasoning models (gpt-4.1, gpt-4o, ...) 400 with # "Unsupported parameter: 'reasoning.effort' is not supported with this model". # Drop the kwarg for those rather than crash the run. -_OPENAI_REASONING_MODEL = re.compile(r"^(gpt-5|o[1-9])") +_OPENAI_REASONING_MODEL = re.compile(r"^(?:gpt-(?:[5-9]|[1-9]\d)|o[1-9])(?:[.-]|$)") def _supports_reasoning_effort(model: str) -> bool: